1 : <?php
2 : /*--------------------------------------------------------------------------+
3 : This file is part of eStudy.
4 : common/classes/class.stringparser.inc.php
5 : - Modulgruppe: Framework
6 : - Beschreibung: BBCode String-Parser
7 : - Version: 0.3, 20/01/09
8 : - Autor(en): Christian Seiler <spam@christian-seiler.de>
9 : Clemens Weiß <clemens.weiss@mni.fh-giessen.de>
10 : Daniel Knapp <daniel.knapp@mni.fh-giessen.de>
11 : +---------------------------------------------------------------------------+
12 : This program is free software; you can redistribute it and/or
13 : modify it under the terms of the GNU General Public License
14 : as published by the Free Software Foundation; either version 2
15 : of the License, or any later version.
16 : +---------------------------------------------------------------------------+
17 : This program is distributed in the hope that it will be useful,
18 : but WITHOUT ANY WARRANTY; without even the implied warranty of
19 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 : GNU General Public License for more details.
21 : You should have received a copy of the GNU General Public License
22 : Along with this program; if not, write to the Free Software
23 : Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24 : +--------------------------------------------------------------------------*/
25 : /**
26 : * Generic string parsing infrastructure
27 : *
28 : * These classes provide the means to parse any kind of string into a tree-like
29 : * memory structure. It would e.g. be possible to create an HTML parser based
30 : * upon this class.
31 : *
32 : * Version: 0.3.3
33 : *
34 : * @author Christian Seiler <spam@christian-seiler.de>
35 : * @copyright Christian Seiler 2004-2008
36 : * @package stringparser
37 : *
38 : * The MIT License
39 : *
40 : * Copyright (c) 2004-2009 Christian Seiler
41 : *
42 : * Permission is hereby granted, free of charge, to any person obtaining a copy
43 : * of this software and associated documentation files (the "Software"), to deal
44 : * in the Software without restriction, including without limitation the rights
45 : * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
46 : * copies of the Software, and to permit persons to whom the Software is
47 : * furnished to do so, subject to the following conditions:
48 : *
49 : * The above copyright notice and this permission notice shall be included in
50 : * all copies or substantial portions of the Software.
51 : *
52 : * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
53 : * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
54 : * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
55 : * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
56 : * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
57 : * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
58 : * THE SOFTWARE.
59 : */
60 :
61 : /**
62 : * String parser mode: Search for the next character
63 : * @see StringParser::_parserMode
64 : */
65 1 : define ('STRINGPARSER_MODE_SEARCH', 1);
66 : /**
67 : * String parser mode: Look at each character of the string
68 : * @see StringParser::_parserMode
69 : */
70 1 : define ('STRINGPARSER_MODE_LOOP', 2);
71 : /**
72 : * Filter type: Prefilter
73 : * @see StringParser::addFilter, StringParser::_prefilters
74 : */
75 1 : define ('STRINGPARSER_FILTER_PRE', 1);
76 : /**
77 : * Filter type: Postfilter
78 : * @see StringParser::addFilter, StringParser::_postfilters
79 : */
80 1 : define ('STRINGPARSER_FILTER_POST', 2);
81 :
82 : /**
83 : * Generic string parser class
84 : *
85 : * This is an abstract class for any type of string parser.
86 : *
87 : * @package stringparser
88 : */
89 1 : class StringParser {
90 : /**
91 : * String parser mode
92 : *
93 : * There are two possible modes: searchmode and loop mode. In loop mode
94 : * every single character is looked at in a loop and it is then decided
95 : * what action to take. This is the most straight-forward approach to
96 : * string parsing but due to the nature of PHP as a scripting language,
97 : * it can also cost performance. In search mode the class posseses a
98 : * list of relevant characters for parsing and uses the
99 : * {@link PHP_MANUAL#strpos strpos} function to search for the next
100 : * relevant character. The search mode will be faster than the loop mode
101 : * in most circumstances but it is also more difficult to implement.
102 : * The subclass that does the string parsing itself will define which
103 : * mode it will implement.
104 : *
105 : * @access protected
106 : * @var int
107 : * @see STRINGPARSER_MODE_SEARCH, STRINGPARSER_MODE_LOOP
108 : */
109 : var $_parserMode = STRINGPARSER_MODE_SEARCH;
110 :
111 : /**
112 : * Raw text
113 : * @access protected
114 : * @var string
115 : */
116 : var $_text = '';
117 :
118 : /**
119 : * Parse stack
120 : * @access protected
121 : * @var array
122 : */
123 : var $_stack = array ();
124 :
125 : /**
126 : * Current position in raw text
127 : * @access protected
128 : * @var integer
129 : */
130 : var $_cpos = -1;
131 :
132 : /**
133 : * Root node
134 : * @access protected
135 : * @var mixed
136 : */
137 : var $_root = null;
138 :
139 : /**
140 : * Length of the text
141 : * @access protected
142 : * @var integer
143 : */
144 : var $_length = -1;
145 :
146 : /**
147 : * Flag if this object is already parsing a text
148 : *
149 : * This flag is to prevent recursive calls to the parse() function that
150 : * would cause very nasty things.
151 : *
152 : * @access protected
153 : * @var boolean
154 : */
155 : var $_parsing = false;
156 :
157 : /**
158 : * Strict mode
159 : *
160 : * Whether to stop parsing if a parse error occurs.
161 : *
162 : * @access public
163 : * @var boolean
164 : */
165 : var $strict = false;
166 :
167 : /**
168 : * Characters or strings to look for
169 : * @access protected
170 : * @var array
171 : */
172 : var $_charactersSearch = array ();
173 :
174 : /**
175 : * Characters currently allowed
176 : *
177 : * Note that this will only be evaluated in loop mode; in search mode
178 : * this would ruin every performance increase. Note that only single
179 : * characters are permitted here, no strings. Please also note that in
180 : * loop mode, {@link StringParser::_charactersSearch _charactersSearch}
181 : * is evaluated before this variable.
182 : *
183 : * If in strict mode, parsing is stopped if a character that is not
184 : * allowed is encountered. If not in strict mode, the character is
185 : * simply ignored.
186 : *
187 : * @access protected
188 : * @var array
189 : */
190 : var $_charactersAllowed = array ();
191 :
192 : /**
193 : * Current parser status
194 : * @access protected
195 : * @var int
196 : */
197 : var $_status = 0;
198 :
199 : /**
200 : * Prefilters
201 : * @access protected
202 : * @var array
203 : */
204 : var $_prefilters = array ();
205 :
206 : /**
207 : * Postfilters
208 : * @access protected
209 : * @var array
210 : */
211 : var $_postfilters = array ();
212 :
213 : /**
214 : * Recently reparsed?
215 : * @access protected
216 : * @var bool
217 : */
218 : var $_recentlyReparsed = false;
219 :
220 : /**
221 : * Constructor
222 : *
223 : * @access public
224 : */
225 : function StringParser () {
226 0 : }
227 :
228 : /**
229 : * Add a filter
230 : *
231 : * @access public
232 : * @param int $type The type of the filter
233 : * @param mixed $callback The callback to call
234 : * @return bool
235 : * @see STRINGPARSER_FILTER_PRE, STRINGPARSER_FILTER_POST
236 : */
237 : function addFilter ($type, $callback) {
238 : // make sure the function is callable
239 0 : if (!is_callable ($callback)) {
240 0 : return false;
241 : }
242 :
243 : switch ($type) {
244 0 : case STRINGPARSER_FILTER_PRE:
245 0 : $this->_prefilters[] = $callback;
246 0 : break;
247 0 : case STRINGPARSER_FILTER_POST:
248 0 : $this->_postfilters[] = $callback;
249 0 : break;
250 0 : default:
251 0 : return false;
252 0 : }
253 :
254 0 : return true;
255 : }
256 :
257 : /**
258 : * Remove all filters
259 : *
260 : * @access public
261 : * @param int $type The type of the filter or 0 for all
262 : * @return bool
263 : * @see STRINGPARSER_FILTER_PRE, STRINGPARSER_FILTER_POST
264 : */
265 : function clearFilters ($type = 0) {
266 : switch ($type) {
267 0 : case 0:
268 0 : $this->_prefilters = array ();
269 0 : $this->_postfilters = array ();
270 0 : break;
271 0 : case STRINGPARSER_FILTER_PRE:
272 0 : $this->_prefilters = array ();
273 0 : break;
274 0 : case STRINGPARSER_FILTER_POST:
275 0 : $this->_postfilters = array ();
276 0 : break;
277 0 : default:
278 0 : return false;
279 0 : }
280 0 : return true;
281 : }
282 :
283 : /**
284 : * This function parses the text
285 : *
286 : * @access public
287 : * @param string $text The text to parse
288 : * @return mixed Either the root object of the tree if no output method
289 : * is defined, the tree reoutput to e.g. a string or false
290 : * if an internal error occured, such as a parse error if
291 : * in strict mode or the object is already parsing a text.
292 : */
293 : function parse ($text) {
294 0 : if ($this->_parsing) {
295 0 : return false;
296 : }
297 0 : $this->_parsing = true;
298 0 : $this->_text = $this->_applyPrefilters ($text);
299 0 : $this->_output = null;
300 0 : $this->_length = strlen ($this->_text);
301 0 : $this->_cpos = 0;
302 0 : unset ($this->_stack);
303 0 : $this->_stack = array ();
304 0 : if (is_object ($this->_root)) {
305 0 : StringParser_Node::destroyNode ($this->_root);
306 0 : }
307 0 : unset ($this->_root);
308 0 : $this->_root = new StringParser_Node_Root ();
309 0 : $this->_stack[0] =& $this->_root;
310 :
311 0 : $this->_parserInit ();
312 :
313 0 : $finished = false;
314 :
315 0 : while (!$finished) {
316 0 : switch ($this->_parserMode) {
317 0 : case STRINGPARSER_MODE_SEARCH:
318 0 : $res = $this->_searchLoop ();
319 0 : if (!$res) {
320 0 : $this->_parsing = false;
321 0 : return false;
322 : }
323 0 : break;
324 0 : case STRINGPARSER_MODE_LOOP:
325 0 : $res = $this->_loop ();
326 0 : if (!$res) {
327 0 : $this->_parsing = false;
328 0 : return false;
329 : }
330 0 : break;
331 0 : default:
332 0 : $this->_parsing = false;
333 0 : return false;
334 0 : }
335 :
336 0 : $res = $this->_closeRemainingBlocks ();
337 0 : if (!$res) {
338 0 : if ($this->strict) {
339 0 : $this->_parsing = false;
340 0 : return false;
341 : } else {
342 0 : $res = $this->_reparseAfterCurrentBlock ();
343 0 : if (!$res) {
344 0 : $this->_parsing = false;
345 0 : return false;
346 : }
347 0 : continue;
348 : }
349 : }
350 0 : $finished = true;
351 0 : }
352 :
353 0 : $res = $this->_modifyTree ();
354 :
355 0 : if (!$res) {
356 0 : $this->_parsing = false;
357 0 : return false;
358 : }
359 :
360 0 : $res = $this->_outputTree ();
361 :
362 0 : if (!$res) {
363 0 : $this->_parsing = false;
364 0 : return false;
365 : }
366 :
367 0 : if (is_null ($this->_output)) {
368 0 : $root =& $this->_root;
369 0 : unset ($this->_root);
370 0 : $this->_root = null;
371 0 : while (count ($this->_stack)) {
372 0 : unset ($this->_stack[count($this->_stack)-1]);
373 0 : }
374 0 : $this->_stack = array ();
375 0 : $this->_parsing = false;
376 0 : return $root;
377 : }
378 :
379 0 : $res = StringParser_Node::destroyNode ($this->_root);
380 0 : if (!$res) {
381 0 : $this->_parsing = false;
382 0 : return false;
383 : }
384 0 : unset ($this->_root);
385 0 : $this->_root = null;
386 0 : while (count ($this->_stack)) {
387 0 : unset ($this->_stack[count($this->_stack)-1]);
388 0 : }
389 0 : $this->_stack = array ();
390 :
391 0 : $this->_parsing = false;
392 0 : return $this->_output;
393 : }
394 :
395 : /**
396 : * Apply prefilters
397 : *
398 : * It is possible to specify prefilters for the parser to do some
399 : * manipulating of the string beforehand.
400 : */
401 : function _applyPrefilters ($text) {
402 0 : foreach ($this->_prefilters as $filter) {
403 0 : if (is_callable ($filter)) {
404 0 : $ntext = call_user_func ($filter, $text);
405 0 : if (is_string ($ntext)) {
406 0 : $text = $ntext;
407 0 : }
408 0 : }
409 0 : }
410 0 : return $text;
411 : }
412 :
413 : /**
414 : * Apply postfilters
415 : *
416 : * It is possible to specify postfilters for the parser to do some
417 : * manipulating of the string afterwards.
418 : */
419 : function _applyPostfilters ($text) {
420 0 : foreach ($this->_postfilters as $filter) {
421 0 : if (is_callable ($filter)) {
422 0 : $ntext = call_user_func ($filter, $text);
423 0 : if (is_string ($ntext)) {
424 0 : $text = $ntext;
425 0 : }
426 0 : }
427 0 : }
428 0 : return $text;
429 : }
430 :
431 : /**
432 : * Abstract method: Manipulate the tree
433 : * @access protected
434 : * @return bool
435 : */
436 : function _modifyTree () {
437 0 : return true;
438 : }
439 :
440 : /**
441 : * Abstract method: Output tree
442 : * @access protected
443 : * @return bool
444 : */
445 : function _outputTree () {
446 : // this could e.g. call _applyPostfilters
447 0 : return true;
448 : }
449 :
450 : /**
451 : * Restart parsing after current block
452 : *
453 : * To achieve this the current top stack object is removed from the
454 : * tree. Then the current item
455 : *
456 : * @access protected
457 : * @return bool
458 : */
459 : function _reparseAfterCurrentBlock () {
460 : // this should definitely not happen!
461 0 : if (($stack_count = count ($this->_stack)) < 2) {
462 0 : return false;
463 : }
464 0 : $topelem =& $this->_stack[$stack_count-1];
465 :
466 0 : $node_parent =& $topelem->_parent;
467 : // remove the child from the tree
468 0 : $res = $node_parent->removeChild ($topelem, false);
469 0 : if (!$res) {
470 0 : return false;
471 : }
472 0 : $res = $this->_popNode ();
473 0 : if (!$res) {
474 0 : return false;
475 : }
476 :
477 : // now try to get the position of the object
478 0 : if ($topelem->occurredAt < 0) {
479 0 : return false;
480 : }
481 : // HACK: could it be necessary to set a different status
482 : // if yes, how should this be achieved? Another member of
483 : // StringParser_Node?
484 0 : $this->_setStatus (0);
485 0 : $res = $this->_appendText ($this->_text{$topelem->occurredAt});
486 0 : if (!$res) {
487 0 : return false;
488 : }
489 :
490 0 : $this->_cpos = $topelem->occurredAt + 1;
491 0 : $this->_recentlyReparsed = true;
492 :
493 0 : return true;
494 : }
495 :
496 : /**
497 : * Abstract method: Close remaining blocks
498 : * @access protected
499 : */
500 : function _closeRemainingBlocks () {
501 : // everything closed
502 0 : if (count ($this->_stack) == 1) {
503 0 : return true;
504 : }
505 : // not everything closed
506 0 : if ($this->strict) {
507 0 : return false;
508 : }
509 0 : while (count ($this->_stack) > 1) {
510 0 : $res = $this->_popNode ();
511 0 : if (!$res) {
512 0 : return false;
513 : }
514 0 : }
515 0 : return true;
516 : }
517 :
518 : /**
519 : * Abstract method: Initialize the parser
520 : * @access protected
521 : */
522 : function _parserInit () {
523 0 : $this->_setStatus (0);
524 0 : }
525 :
526 : /**
527 : * Abstract method: Set a specific status
528 : * @access protected
529 : */
530 : function _setStatus ($status) {
531 0 : if ($status != 0) {
532 0 : return false;
533 : }
534 0 : $this->_charactersSearch = array ();
535 0 : $this->_charactersAllowed = array ();
536 0 : $this->_status = $status;
537 0 : return true;
538 : }
539 :
540 : /**
541 : * Abstract method: Handle status
542 : * @access protected
543 : * @param int $status The current status
544 : * @param string $needle The needle that was found
545 : * @return bool
546 : */
547 : function _handleStatus ($status, $needle) {
548 0 : $this->_appendText ($needle);
549 0 : $this->_cpos += strlen ($needle);
550 0 : return true;
551 : }
552 :
553 : /**
554 : * Search mode loop
555 : * @access protected
556 : * @return bool
557 : */
558 : function _searchLoop () {
559 0 : $i = 0;
560 0 : while (1) {
561 : // make sure this is false!
562 0 : $this->_recentlyReparsed = false;
563 :
564 0 : list ($needle, $offset) = $this->_strpos ($this->_charactersSearch, $this->_cpos);
565 : // parser ends here
566 0 : if ($needle === false) {
567 : // original status 0 => no problem
568 0 : if (!$this->_status) {
569 0 : break;
570 : }
571 : // not in original status? strict mode?
572 0 : if ($this->strict) {
573 0 : return false;
574 : }
575 : // break up parsing operation of current node
576 0 : $res = $this->_reparseAfterCurrentBlock ();
577 0 : if (!$res) {
578 0 : return false;
579 : }
580 0 : continue;
581 : }
582 : // get subtext
583 0 : $subtext = substr ($this->_text, $this->_cpos, $offset - $this->_cpos);
584 0 : $res = $this->_appendText ($subtext);
585 0 : if (!$res) {
586 0 : return false;
587 : }
588 0 : $this->_cpos = $offset;
589 0 : $res = $this->_handleStatus ($this->_status, $needle);
590 0 : if (!$res && $this->strict) {
591 0 : return false;
592 : }
593 0 : if (!$res) {
594 0 : $res = $this->_appendText ($this->_text{$this->_cpos});
595 0 : if (!$res) {
596 0 : return false;
597 : }
598 0 : $this->_cpos++;
599 0 : continue;
600 : }
601 0 : if ($this->_recentlyReparsed) {
602 0 : $this->_recentlyReparsed = false;
603 0 : continue;
604 : }
605 0 : $this->_cpos += strlen ($needle);
606 0 : }
607 :
608 : // get subtext
609 0 : if ($this->_cpos < strlen ($this->_text)) {
610 0 : $subtext = substr ($this->_text, $this->_cpos);
611 0 : $res = $this->_appendText ($subtext);
612 0 : if (!$res) {
613 0 : return false;
614 : }
615 0 : }
616 :
617 0 : return true;
618 : }
619 :
620 : /**
621 : * Loop mode loop
622 : *
623 : * @access protected
624 : * @return bool
625 : */
626 : function _loop () {
627 : // HACK: This method ist not yet implemented correctly, the code below
628 : // DOES NOT WORK! Do not use!
629 :
630 0 : return false;
631 : /*
632 : while ($this->_cpos < $this->_length) {
633 : $needle = $this->_strDetect ($this->_charactersSearch, $this->_cpos);
634 :
635 : if ($needle === false) {
636 : // not found => see if character is allowed
637 : if (!in_array ($this->_text{$this->_cpos}, $this->_charactersAllowed)) {
638 : if ($strict) {
639 : return false;
640 : }
641 : // ignore
642 : continue;
643 : }
644 : // lot's of FIXMES
645 : $res = $this->_appendText ($this->_text{$this->_cpos});
646 : if (!$res) {
647 : return false;
648 : }
649 : }
650 :
651 : // get subtext
652 : $subtext = substr ($this->_text, $offset, $offset - $this->_cpos);
653 : $res = $this->_appendText ($subtext);
654 : if (!$res) {
655 : return false;
656 : }
657 : $this->_cpos = $subtext;
658 : $res = $this->_handleStatus ($this->_status, $needle);
659 : if (!$res && $strict) {
660 : return false;
661 : }
662 : }
663 : // original status 0 => no problem
664 : if (!$this->_status) {
665 : return true;
666 : }
667 : // not in original status? strict mode?
668 : if ($this->strict) {
669 : return false;
670 : }
671 : // break up parsing operation of current node
672 : $res = $this->_reparseAfterCurrentBlock ();
673 : if (!$res) {
674 : return false;
675 : }
676 : // this will not cause an infinite loop because
677 : // _reparseAfterCurrentBlock will increase _cpos by one!
678 : return $this->_loop ();
679 : */
680 : }
681 :
682 : /**
683 : * Abstract method Append text depending on current status
684 : * @access protected
685 : * @param string $text The text to append
686 : * @return bool On success, the function returns true, else false
687 : */
688 : function _appendText ($text) {
689 0 : if (!strlen ($text)) {
690 0 : return true;
691 : }
692 : // default: call _appendToLastTextChild
693 0 : return $this->_appendToLastTextChild ($text);
694 : }
695 :
696 : /**
697 : * Append text to last text child of current top parser stack node
698 : * @access protected
699 : * @param string $text The text to append
700 : * @return bool On success, the function returns true, else false
701 : */
702 : function _appendToLastTextChild ($text) {
703 0 : $scount = count ($this->_stack);
704 0 : if ($scount == 0) {
705 0 : return false;
706 : }
707 0 : return $this->_stack[$scount-1]->appendToLastTextChild ($text);
708 : }
709 :
710 : /**
711 : * Searches {@link StringParser::_text _text} for every needle that is
712 : * specified by using the {@link PHP_MANUAL#strpos strpos} function. It
713 : * returns an associative array with the key <code>'needle'</code>
714 : * pointing at the string that was found first and the key
715 : * <code>'offset'</code> pointing at the offset at which the string was
716 : * found first. If no needle was found, the <code>'needle'</code>
717 : * element is <code>false</code> and the <code>'offset'</code> element
718 : * is <code>-1</code>.
719 : *
720 : * @access protected
721 : * @param array $needles
722 : * @param int $offset
723 : * @return array
724 : * @see StringParser::_text
725 : */
726 : function _strpos ($needles, $offset) {
727 0 : $cur_needle = false;
728 0 : $cur_offset = -1;
729 :
730 0 : if ($offset < strlen ($this->_text)) {
731 0 : foreach ($needles as $needle) {
732 0 : $n_offset = strpos ($this->_text, $needle, $offset);
733 0 : if ($n_offset !== false && ($n_offset < $cur_offset || $cur_offset < 0)) {
734 0 : $cur_needle = $needle;
735 0 : $cur_offset = $n_offset;
736 0 : }
737 0 : }
738 0 : }
739 :
740 0 : return array ($cur_needle, $cur_offset, 'needle' => $cur_needle, 'offset' => $cur_offset);
741 : }
742 :
743 : /**
744 : * Detects a string at the current position
745 : *
746 : * @access protected
747 : * @param array $needles The strings that are to be detected
748 : * @param int $offset The current offset
749 : * @return mixed The string that was detected or the needle
750 : */
751 : function _strDetect ($needles, $offset) {
752 0 : foreach ($needles as $needle) {
753 0 : $l = strlen ($needle);
754 0 : if (substr ($this->_text, $offset, $l) == $needle) {
755 0 : return $needle;
756 : }
757 0 : }
758 0 : return false;
759 : }
760 :
761 :
762 : /**
763 : * Adds a node to the current parse stack
764 : *
765 : * @access protected
766 : * @param object $node The node that is to be added
767 : * @return bool True on success, else false.
768 : * @see StringParser_Node, StringParser::_stack
769 : */
770 : function _pushNode (&$node) {
771 0 : $stack_count = count ($this->_stack);
772 0 : $max_node =& $this->_stack[$stack_count-1];
773 0 : if (!$max_node->appendChild ($node)) {
774 0 : return false;
775 : }
776 0 : $this->_stack[$stack_count] =& $node;
777 0 : return true;
778 : }
779 :
780 : /**
781 : * Removes a node from the current parse stack
782 : *
783 : * @access protected
784 : * @return bool True on success, else false.
785 : * @see StringParser_Node, StringParser::_stack
786 : */
787 : function _popNode () {
788 0 : $stack_count = count ($this->_stack);
789 0 : unset ($this->_stack[$stack_count-1]);
790 0 : return true;
791 : }
792 :
793 : /**
794 : * Execute a method on the top element
795 : *
796 : * @access protected
797 : * @return mixed
798 : */
799 : function _topNode () {
800 0 : $args = func_get_args ();
801 0 : if (!count ($args)) {
802 0 : return; // oops?
803 : }
804 0 : $method = array_shift ($args);
805 0 : $stack_count = count ($this->_stack);
806 0 : $method = array (&$this->_stack[$stack_count-1], $method);
807 0 : if (!is_callable ($method)) {
808 0 : return; // oops?
809 : }
810 0 : return call_user_func_array ($method, $args);
811 : }
812 :
813 : /**
814 : * Get a variable of the top element
815 : *
816 : * @access protected
817 : * @return mixed
818 : */
819 : function _topNodeVar ($var) {
820 0 : $stack_count = count ($this->_stack);
821 0 : return $this->_stack[$stack_count-1]->$var;
822 : }
823 : }
824 :
825 : /**
826 : * Node type: Unknown node
827 : * @see StringParser_Node::_type
828 : */
829 1 : define ('STRINGPARSER_NODE_UNKNOWN', 0);
830 :
831 : /**
832 : * Node type: Root node
833 : * @see StringParser_Node::_type
834 : */
835 1 : define ('STRINGPARSER_NODE_ROOT', 1);
836 :
837 : /**
838 : * Node type: Text node
839 : * @see StringParser_Node::_type
840 : */
841 1 : define ('STRINGPARSER_NODE_TEXT', 2);
842 :
843 : /**
844 : * Global value that is a counter of string parser node ids. Compare it to a
845 : * sequence in databases.
846 : * @var int
847 : */
848 1 : $GLOBALS['__STRINGPARSER_NODE_ID'] = 0;
849 :
850 : /**
851 : * Generic string parser node class
852 : *
853 : * This is an abstract class for any type of node that is used within the
854 : * string parser. General warning: This class contains code regarding references
855 : * that is very tricky. Please do not touch this code unless you exactly know
856 : * what you are doing. Incorrect handling of references may cause PHP to crash
857 : * with a segmentation fault! You have been warned.
858 : *
859 : * @package stringparser
860 : */
861 1 : class StringParser_Node {
862 : /**
863 : * The type of this node.
864 : *
865 : * There are three standard node types: root node, text node and unknown
866 : * node. All node types are integer constants. Any node type of a
867 : * subclass must be at least 32 to allow future developements.
868 : *
869 : * @access protected
870 : * @var int
871 : * @see STRINGPARSER_NODE_ROOT, STRINGPARSER_NODE_TEXT
872 : * @see STRINGPARSER_NODE_UNKNOWN
873 : */
874 : var $_type = STRINGPARSER_NODE_UNKNOWN;
875 :
876 : /**
877 : * The node ID
878 : *
879 : * This ID uniquely identifies this node. This is needed when searching
880 : * for a specific node in the children array. Please note that this is
881 : * only an internal variable and should never be used - not even in
882 : * subclasses and especially not in external data structures. This ID
883 : * has nothing to do with any type of ID in HTML oder XML.
884 : *
885 : * @access protected
886 : * @var int
887 : * @see StringParser_Node::_children
888 : */
889 : var $_id = -1;
890 :
891 : /**
892 : * The parent of this node.
893 : *
894 : * It is either null (root node) or a reference to the parent object.
895 : *
896 : * @access protected
897 : * @var mixed
898 : * @see StringParser_Node::_children
899 : */
900 : var $_parent = null;
901 :
902 : /**
903 : * The children of this node.
904 : *
905 : * It contains an array of references to all the children nodes of this
906 : * node.
907 : *
908 : * @access protected
909 : * @var array
910 : * @see StringParser_Node::_parent
911 : */
912 : var $_children = array ();
913 :
914 : /**
915 : * Occured at
916 : *
917 : * This defines the position in the parsed text where this node occurred
918 : * at. If -1, this value was not possible to be determined.
919 : *
920 : * @access public
921 : * @var int
922 : */
923 : var $occurredAt = -1;
924 :
925 : /**
926 : * Constructor
927 : *
928 : * Currently, the constructor only allocates a new ID for the node and
929 : * assigns it.
930 : *
931 : * @access public
932 : * @param int $occurredAt The position in the text where this node
933 : * occurred at. If not determinable, it is -1.
934 : * @global __STRINGPARSER_NODE_ID
935 : */
936 : function StringParser_Node ($occurredAt = -1) {
937 0 : $this->_id = $GLOBALS['__STRINGPARSER_NODE_ID']++;
938 0 : $this->occurredAt = $occurredAt;
939 0 : }
940 :
941 : /**
942 : * Type of the node
943 : *
944 : * This function returns the type of the node
945 : *
946 : * @access public
947 : * @return int
948 : */
949 : function type () {
950 0 : return $this->_type;
951 : }
952 :
953 : /**
954 : * Prepend a node
955 : *
956 : * @access public
957 : * @param object $node The node to be prepended.
958 : * @return bool On success, the function returns true, else false.
959 : */
960 : function prependChild (&$node) {
961 0 : if (!is_object ($node)) {
962 0 : return false;
963 : }
964 :
965 : // root nodes may not be children of other nodes!
966 0 : if ($node->_type == STRINGPARSER_NODE_ROOT) {
967 0 : return false;
968 : }
969 :
970 : // if node already has a parent
971 0 : if ($node->_parent !== false) {
972 : // remove node from there
973 0 : $parent =& $node->_parent;
974 0 : if (!$parent->removeChild ($node, false)) {
975 0 : return false;
976 : }
977 0 : unset ($parent);
978 0 : }
979 :
980 0 : $index = count ($this->_children) - 1;
981 : // move all nodes to a new index
982 0 : while ($index >= 0) {
983 : // save object
984 0 : $object =& $this->_children[$index];
985 : // we have to unset it because else it will be
986 : // overridden in in the loop
987 0 : unset ($this->_children[$index]);
988 : // put object to new position
989 0 : $this->_children[$index+1] =& $object;
990 0 : $index--;
991 0 : }
992 0 : $this->_children[0] =& $node;
993 0 : return true;
994 : }
995 :
996 : /**
997 : * Append text to last text child
998 : * @access public
999 : * @param string $text The text to append
1000 : * @return bool On success, the function returns true, else false
1001 : */
1002 : function appendToLastTextChild ($text) {
1003 0 : $ccount = count ($this->_children);
1004 0 : if ($ccount == 0 || $this->_children[$ccount-1]->_type != STRINGPARSER_NODE_TEXT) {
1005 0 : $ntextnode = new StringParser_Node_Text ($text);
1006 0 : return $this->appendChild ($ntextnode);
1007 : } else {
1008 0 : $this->_children[$ccount-1]->appendText ($text);
1009 0 : return true;
1010 : }
1011 : }
1012 :
1013 : /**
1014 : * Append a node to the children
1015 : *
1016 : * This function appends a node to the children array(). It
1017 : * automatically sets the {@link StrinParser_Node::_parent _parent}
1018 : * property of the node that is to be appended.
1019 : *
1020 : * @access public
1021 : * @param object $node The node that is to be appended.
1022 : * @return bool On success, the function returns true, else false.
1023 : */
1024 : function appendChild (&$node) {
1025 0 : if (!is_object ($node)) {
1026 0 : return false;
1027 : }
1028 :
1029 : // root nodes may not be children of other nodes!
1030 0 : if ($node->_type == STRINGPARSER_NODE_ROOT) {
1031 0 : return false;
1032 : }
1033 :
1034 : // if node already has a parent
1035 0 : if ($node->_parent !== null) {
1036 : // remove node from there
1037 0 : $parent =& $node->_parent;
1038 0 : if (!$parent->removeChild ($node, false)) {
1039 0 : return false;
1040 : }
1041 0 : unset ($parent);
1042 0 : }
1043 :
1044 : // append it to current node
1045 0 : $new_index = count ($this->_children);
1046 0 : $this->_children[$new_index] =& $node;
1047 0 : $node->_parent =& $this;
1048 0 : return true;
1049 : }
1050 :
1051 : /**
1052 : * Insert a node before another node
1053 : *
1054 : * @access public
1055 : * @param object $node The node to be inserted.
1056 : * @param object $reference The reference node where the new node is
1057 : * to be inserted before.
1058 : * @return bool On success, the function returns true, else false.
1059 : */
1060 : function insertChildBefore (&$node, &$reference) {
1061 0 : if (!is_object ($node)) {
1062 0 : return false;
1063 : }
1064 :
1065 : // root nodes may not be children of other nodes!
1066 0 : if ($node->_type == STRINGPARSER_NODE_ROOT) {
1067 0 : return false;
1068 : }
1069 :
1070 : // is the reference node a child?
1071 0 : $child = $this->_findChild ($reference);
1072 :
1073 0 : if ($child === false) {
1074 0 : return false;
1075 : }
1076 :
1077 : // if node already has a parent
1078 0 : if ($node->_parent !== null) {
1079 : // remove node from there
1080 0 : $parent =& $node->_parent;
1081 0 : if (!$parent->removeChild ($node, false)) {
1082 0 : return false;
1083 : }
1084 0 : unset ($parent);
1085 0 : }
1086 :
1087 0 : $index = count ($this->_children) - 1;
1088 : // move all nodes to a new index
1089 0 : while ($index >= $child) {
1090 : // save object
1091 0 : $object =& $this->_children[$index];
1092 : // we have to unset it because else it will be
1093 : // overridden in in the loop
1094 0 : unset ($this->_children[$index]);
1095 : // put object to new position
1096 0 : $this->_children[$index+1] =& $object;
1097 0 : $index--;
1098 0 : }
1099 0 : $this->_children[$child] =& $node;
1100 0 : return true;
1101 : }
1102 :
1103 : /**
1104 : * Insert a node after another node
1105 : *
1106 : * @access public
1107 : * @param object $node The node to be inserted.
1108 : * @param object $reference The reference node where the new node is
1109 : * to be inserted after.
1110 : * @return bool On success, the function returns true, else false.
1111 : */
1112 : function insertChildAfter (&$node, &$reference) {
1113 0 : if (!is_object ($node)) {
1114 0 : return false;
1115 : }
1116 :
1117 : // root nodes may not be children of other nodes!
1118 0 : if ($node->_type == STRINGPARSER_NODE_ROOT) {
1119 0 : return false;
1120 : }
1121 :
1122 : // is the reference node a child?
1123 0 : $child = $this->_findChild ($reference);
1124 :
1125 0 : if ($child === false) {
1126 0 : return false;
1127 : }
1128 :
1129 : // if node already has a parent
1130 0 : if ($node->_parent !== false) {
1131 : // remove node from there
1132 0 : $parent =& $node->_parent;
1133 0 : if (!$parent->removeChild ($node, false)) {
1134 0 : return false;
1135 : }
1136 0 : unset ($parent);
1137 0 : }
1138 :
1139 0 : $index = count ($this->_children) - 1;
1140 : // move all nodes to a new index
1141 0 : while ($index >= $child + 1) {
1142 : // save object
1143 0 : $object =& $this->_children[$index];
1144 : // we have to unset it because else it will be
1145 : // overridden in in the loop
1146 0 : unset ($this->_children[$index]);
1147 : // put object to new position
1148 0 : $this->_children[$index+1] =& $object;
1149 0 : $index--;
1150 0 : }
1151 0 : $this->_children[$child + 1] =& $node;
1152 0 : return true;
1153 : }
1154 :
1155 : /**
1156 : * Remove a child node
1157 : *
1158 : * This function removes a child from the children array. A parameter
1159 : * tells the function whether to destroy the child afterwards or not.
1160 : * If the specified node is not a child of this node, the function will
1161 : * return false.
1162 : *
1163 : * @access public
1164 : * @param mixed $child The child to destroy; either an integer
1165 : * specifying the index of the child or a reference
1166 : * to the child itself.
1167 : * @param bool $destroy Destroy the child afterwards.
1168 : * @return bool On success, the function returns true, else false.
1169 : */
1170 : function removeChild (&$child, $destroy = false) {
1171 0 : if (is_object ($child)) {
1172 : // if object: get index
1173 0 : $object =& $child;
1174 0 : unset ($child);
1175 0 : $child = $this->_findChild ($object);
1176 0 : if ($child === false) {
1177 0 : return false;
1178 : }
1179 0 : } else {
1180 : // remove reference on $child
1181 0 : $save = $child;
1182 0 : unset($child);
1183 0 : $child = $save;
1184 :
1185 : // else: get object
1186 0 : if (!isset($this->_children[$child])) {
1187 0 : return false;
1188 : }
1189 0 : $object =& $this->_children[$child];
1190 : }
1191 :
1192 : // store count for later use
1193 0 : $ccount = count ($this->_children);
1194 :
1195 : // index out of bounds
1196 0 : if (!is_int ($child) || $child < 0 || $child >= $ccount) {
1197 0 : return false;
1198 : }
1199 :
1200 : // inkonsistency
1201 0 : if ($this->_children[$child]->_parent === null ||
1202 0 : $this->_children[$child]->_parent->_id != $this->_id) {
1203 0 : return false;
1204 : }
1205 :
1206 : // $object->_parent = null would equal to $this = null
1207 : // as $object->_parent is a reference to $this!
1208 : // because of this, we have to unset the variable to remove
1209 : // the reference and then redeclare the variable
1210 0 : unset ($object->_parent); $object->_parent = null;
1211 :
1212 : // we have to unset it because else it will be overridden in
1213 : // in the loop
1214 0 : unset ($this->_children[$child]);
1215 :
1216 : // move all remaining objects one index higher
1217 0 : while ($child < $ccount - 1) {
1218 : // save object
1219 0 : $obj =& $this->_children[$child+1];
1220 : // we have to unset it because else it will be
1221 : // overridden in in the loop
1222 0 : unset ($this->_children[$child+1]);
1223 : // put object to new position
1224 0 : $this->_children[$child] =& $obj;
1225 : // UNSET THE OBJECT!
1226 0 : unset ($obj);
1227 0 : $child++;
1228 0 : }
1229 :
1230 0 : if ($destroy) {
1231 0 : return StringParser_Node::destroyNode ($object);
1232 : unset ($object);
1233 : }
1234 0 : return true;
1235 : }
1236 :
1237 : /**
1238 : * Get the first child of this node
1239 : *
1240 : * @access public
1241 : * @return mixed
1242 : */
1243 : function &firstChild () {
1244 0 : $ret = null;
1245 0 : if (!count ($this->_children)) {
1246 0 : return $ret;
1247 : }
1248 0 : return $this->_children[0];
1249 : }
1250 :
1251 : /**
1252 : * Get the last child of this node
1253 : *
1254 : * @access public
1255 : * @return mixed
1256 : */
1257 : function &lastChild () {
1258 0 : $ret = null;
1259 0 : $c = count ($this->_children);
1260 0 : if (!$c) {
1261 0 : return $ret;
1262 : }
1263 0 : return $this->_children[$c-1];
1264 : }
1265 :
1266 : /**
1267 : * Destroy a node
1268 : *
1269 : * @access public
1270 : * @static
1271 : * @param object $node The node to destroy
1272 : * @return bool True on success, else false.
1273 : */
1274 : function destroyNode (&$node) {
1275 0 : if ($node === null) {
1276 0 : return false;
1277 : }
1278 : // if parent exists: remove node from tree!
1279 0 : if ($node->_parent !== null) {
1280 0 : $parent =& $node->_parent;
1281 : // directly return that result because the removeChild
1282 : // method will call destroyNode again
1283 0 : return $parent->removeChild ($node, true);
1284 : }
1285 :
1286 : // node has children
1287 0 : while (count ($node->_children)) {
1288 0 : $child = 0;
1289 : // remove first child until no more children remain
1290 0 : if (!$node->removeChild ($child, true)) {
1291 0 : return false;
1292 : }
1293 0 : unset($child);
1294 0 : }
1295 :
1296 : // now call the nodes destructor
1297 0 : if (!$node->_destroy ()) {
1298 0 : return false;
1299 : }
1300 :
1301 : // now just unset it and prey that there are no more references
1302 : // to this node
1303 0 : unset ($node);
1304 :
1305 0 : return true;
1306 : }
1307 :
1308 : /**
1309 : * Destroy this node
1310 : *
1311 : *
1312 : * @access protected
1313 : * @return bool True on success, else false.
1314 : */
1315 : function _destroy () {
1316 0 : return true;
1317 : }
1318 :
1319 : /**
1320 : * Find a child node
1321 : *
1322 : * This function searches for a node in the own children and returns
1323 : * the index of the node or false if the node is not a child of this
1324 : * node.
1325 : *
1326 : * @access protected
1327 : * @param mixed $child The node to look for.
1328 : * @return mixed The index of the child node on success, else false.
1329 : */
1330 : function _findChild (&$child) {
1331 0 : if (!is_object ($child)) {
1332 0 : return false;
1333 : }
1334 :
1335 0 : $ccount = count ($this->_children);
1336 0 : for ($i = 0; $i < $ccount; $i++) {
1337 0 : if ($this->_children[$i]->_id == $child->_id) {
1338 0 : return $i;
1339 : }
1340 0 : }
1341 :
1342 0 : return false;
1343 : }
1344 :
1345 : /**
1346 : * Checks equality of this node and another node
1347 : *
1348 : * @access public
1349 : * @param mixed $node The node to be compared with
1350 : * @return bool True if the other node equals to this node, else false.
1351 : */
1352 : function equals (&$node) {
1353 0 : return ($this->_id == $node->_id);
1354 : }
1355 :
1356 : /**
1357 : * Determines whether a criterium matches this node
1358 : *
1359 : * @access public
1360 : * @param string $criterium The criterium that is to be checked
1361 : * @param mixed $value The value that is to be compared
1362 : * @return bool True if this node matches that criterium
1363 : */
1364 : function matchesCriterium ($criterium, $value) {
1365 0 : return false;
1366 : }
1367 :
1368 : /**
1369 : * Search for nodes with a certain criterium
1370 : *
1371 : * This may be used to implement getElementsByTagName etc.
1372 : *
1373 : * @access public
1374 : * @param string $criterium The criterium that is to be checked
1375 : * @param mixed $value The value that is to be compared
1376 : * @return array All subnodes that match this criterium
1377 : */
1378 : function &getNodesByCriterium ($criterium, $value) {
1379 0 : $nodes = array ();
1380 0 : $node_ctr = 0;
1381 0 : for ($i = 0; $i < count ($this->_children); $i++) {
1382 0 : if ($this->_children[$i]->matchesCriterium ($criterium, $value)) {
1383 0 : $nodes[$node_ctr++] =& $this->_children[$i];
1384 0 : }
1385 0 : $subnodes = $this->_children[$i]->getNodesByCriterium ($criterium, $value);
1386 0 : if (count ($subnodes)) {
1387 0 : $subnodes_count = count ($subnodes);
1388 0 : for ($j = 0; $j < $subnodes_count; $j++) {
1389 0 : $nodes[$node_ctr++] =& $subnodes[$j];
1390 0 : unset ($subnodes[$j]);
1391 0 : }
1392 0 : }
1393 0 : unset ($subnodes);
1394 0 : }
1395 0 : return $nodes;
1396 : }
1397 :
1398 : /**
1399 : * Search for nodes with a certain criterium and return the count
1400 : *
1401 : * Similar to getNodesByCriterium
1402 : *
1403 : * @access public
1404 : * @param string $criterium The criterium that is to be checked
1405 : * @param mixed $value The value that is to be compared
1406 : * @return int The number of subnodes that match this criterium
1407 : */
1408 : function getNodeCountByCriterium ($criterium, $value) {
1409 0 : $node_ctr = 0;
1410 0 : for ($i = 0; $i < count ($this->_children); $i++) {
1411 0 : if ($this->_children[$i]->matchesCriterium ($criterium, $value)) {
1412 0 : $node_ctr++;
1413 0 : }
1414 0 : $subnodes = $this->_children[$i]->getNodeCountByCriterium ($criterium, $value);
1415 0 : $node_ctr += $subnodes;
1416 0 : }
1417 0 : return $node_ctr;
1418 : }
1419 :
1420 : /**
1421 : * Dump nodes
1422 : *
1423 : * This dumps a tree of nodes
1424 : *
1425 : * @access public
1426 : * @param string $prefix The prefix that is to be used for indentation
1427 : * @param string $linesep The line separator
1428 : * @param int $level The initial level of indentation
1429 : * @return string
1430 : */
1431 : function dump ($prefix = " ", $linesep = "\n", $level = 0) {
1432 0 : $str = str_repeat ($prefix, $level) . $this->_id . ": " . $this->_dumpToString () . $linesep;
1433 0 : for ($i = 0; $i < count ($this->_children); $i++) {
1434 0 : $str .= $this->_children[$i]->dump ($prefix, $linesep, $level + 1);
1435 0 : }
1436 0 : return $str;
1437 : }
1438 :
1439 : /**
1440 : * Dump this node to a string
1441 : *
1442 : * @access protected
1443 : * @return string
1444 : */
1445 : function _dumpToString () {
1446 0 : if ($this->_type == STRINGPARSER_NODE_ROOT) {
1447 0 : return "root";
1448 : }
1449 0 : return (string)$this->_type;
1450 : }
1451 : }
1452 :
1453 : /**
1454 : * String parser root node class
1455 : *
1456 : * @package stringparser
1457 : */
1458 1 : class StringParser_Node_Root extends StringParser_Node {
1459 : /**
1460 : * The type of this node.
1461 : *
1462 : * This node is a root node.
1463 : *
1464 : * @access protected
1465 : * @var int
1466 : * @see STRINGPARSER_NODE_ROOT
1467 : */
1468 : var $_type = STRINGPARSER_NODE_ROOT;
1469 : }
1470 :
1471 : /**
1472 : * String parser text node class
1473 : *
1474 : * @package stringparser
1475 : */
1476 1 : class StringParser_Node_Text extends StringParser_Node {
1477 : /**
1478 : * The type of this node.
1479 : *
1480 : * This node is a text node.
1481 : *
1482 : * @access protected
1483 : * @var int
1484 : * @see STRINGPARSER_NODE_TEXT
1485 : */
1486 : var $_type = STRINGPARSER_NODE_TEXT;
1487 :
1488 : /**
1489 : * Node flags
1490 : *
1491 : * @access protected
1492 : * @var array
1493 : */
1494 : var $_flags = array ();
1495 :
1496 : /**
1497 : * The content of this node
1498 : * @access public
1499 : * @var string
1500 : */
1501 : var $content = '';
1502 :
1503 : /**
1504 : * Constructor
1505 : *
1506 : * @access public
1507 : * @param string $content The initial content of this element
1508 : * @param int $occurredAt The position in the text where this node
1509 : * occurred at. If not determinable, it is -1.
1510 : * @see StringParser_Node_Text::content
1511 : */
1512 : function StringParser_Node_Text ($content, $occurredAt = -1) {
1513 0 : parent::StringParser_Node ($occurredAt);
1514 0 : $this->content = $content;
1515 0 : }
1516 :
1517 : /**
1518 : * Append text to content
1519 : *
1520 : * @access public
1521 : * @param string $text The text to append
1522 : * @see StringParser_Node_Text::content
1523 : */
1524 : function appendText ($text) {
1525 0 : $this->content .= $text;
1526 0 : }
1527 :
1528 : /**
1529 : * Set a flag
1530 : *
1531 : * @access public
1532 : * @param string $name The name of the flag
1533 : * @param mixed $value The value of the flag
1534 : */
1535 : function setFlag ($name, $value) {
1536 0 : $this->_flags[$name] = $value;
1537 0 : return true;
1538 : }
1539 :
1540 : /**
1541 : * Get Flag
1542 : *
1543 : * @access public
1544 : * @param string $flag The requested flag
1545 : * @param string $type The requested type of the return value
1546 : * @param mixed $default The default return value
1547 : */
1548 : function getFlag ($flag, $type = 'mixed', $default = null) {
1549 0 : if (!isset ($this->_flags[$flag])) {
1550 0 : return $default;
1551 : }
1552 0 : $return = $this->_flags[$flag];
1553 0 : if ($type != 'mixed') {
1554 0 : settype ($return, $type);
1555 0 : }
1556 0 : return $return;
1557 : }
1558 :
1559 : /**
1560 : * Dump this node to a string
1561 : */
1562 : function _dumpToString () {
1563 0 : return "text \"".substr (preg_replace ('/\s+/', ' ', $this->content), 0, 40)."\" [f:".preg_replace ('/\s+/', ' ', join(':', array_keys ($this->_flags)))."]";
1564 : }
1565 : }
|